package com.self.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.self.shortlink.project.common.convention.exception.ClientException;
import com.self.shortlink.project.common.convention.exception.ServiceException;
import com.self.shortlink.project.common.enums.VailDateTypeEnum;
import com.self.shortlink.project.config.GotoDomainWhiteListConfiguration;
import com.self.shortlink.project.dao.entity.ShortLinkDO;
import com.self.shortlink.project.dao.entity.ShortLinkGotoDO;
import com.self.shortlink.project.dao.mapper.ShortLinkGotoMapper;
import com.self.shortlink.project.dao.mapper.ShortLinkMapper;
import com.self.shortlink.project.dto.biz.ShortLinkStatsRecordDTO;
import com.self.shortlink.project.dto.req.ShortLinkBatchCreateReqDTO;
import com.self.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.self.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.self.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.self.shortlink.project.dto.resp.*;
import com.self.shortlink.project.handler.CustomBlockHandler;
import com.self.shortlink.project.mq.producer.ShortLinkStatsSaveProducer;
import com.self.shortlink.project.service.ShortLinkService;
import com.self.shortlink.project.toolkit.HashUtil;
import com.self.shortlink.project.toolkit.LinkUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.self.shortlink.project.common.constant.RedisKeyConstant.*;

/**
 * 短链接接口实现层
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final ShortLinkStatsSaveProducer shortLinkStatsSaveProducer;
    private final GotoDomainWhiteListConfiguration gotoDomainWhiteListConfiguration;

    // 定义默认域名 默认为localhost
    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain;

    // 定义默认端口 默认为port：8003
    @Value("${short-link.port.default}")
    private String port;


    // 验证当前访问链接是否在白名单中
    private void verificationWhiteList(String originUrl) {

        // 验证白名单功能开关
        Boolean enable = gotoDomainWhiteListConfiguration.getEnable();
        if (gotoDomainWhiteListConfiguration.getEnable()) {
            if (enable == null || !enable) {
                return;
            }
            // 获取域名中刨除“https”、“http”、“www”的部分
            String domain = LinkUtil.extractDomain(originUrl);
            if (StrUtil.isBlank(domain)) {
                throw new ClientException("域名白名单未配置");
            }
            // 获取所有可访问白名单域名
            List<String> details = gotoDomainWhiteListConfiguration.getDetails();
            // 当前域名不在可访问白名单中
            if (!details.contains(domain)) {
                throw new ClientException("演示环境为避免恶意攻击，请生成以下网站跳转链接：" + gotoDomainWhiteListConfiguration.getNames());
            }
        }
    }

    // Transactional 确保方法内的数据库操作要么全部成功（提交事务），要么全部失败（回滚事务）
    // Sentinel（流量控制组件）
    // value 定义应用到的方法名， blockHandler 定义限流异常处理方法， blockHandlerClass 定义限流异常处理类
    @Transactional(rollbackFor = Exception.class)
    @Override
    @SentinelResource(
            value = "create_short-link",
            blockHandler = "createShortLinkBlockHandlerMethod",
            blockHandlerClass = CustomBlockHandler.class
    )
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        // 验证当前链接是否在可访问白名单中
        verificationWhiteList(requestParam.getOriginUrl());
        String shortLinkSuffix = generateSuffix(requestParam);
        String fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain)
                .append(":")
                .append(port)
                .append("/")
                .append(shortLinkSuffix)
                .toString();
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .domain(createShortLinkDefaultDomain)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .createdType(requestParam.getCreatedType())
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .describe(requestParam.getDescribe())
                .shortUri(shortLinkSuffix)
                .enableStatus(0)
                .clickNum(0)
                .totalPv(0)
                .totalUip(0)
                .totalUv(0)
                .delTime(0L)
                .favicon(getFavicon(requestParam.getOriginUrl()))
                .fullShortUrl(fullShortUrl)
                .build();
        ShortLinkGotoDO shortLinkGotoDO = ShortLinkGotoDO.builder()
                .fullShortUrl(fullShortUrl)
                .gid(requestParam.getGid())
                .build();

        try{
            // 如果短链接存在则抛异常
            baseMapper.insert(shortLinkDO);
            shortLinkGotoMapper.insert(shortLinkGotoDO);
        } catch (DuplicateKeyException ex) {
            throw new ServiceException("短链接：" + fullShortUrl + "重复入库");
        }

        // 设置 redis（短链接跳转） 缓存键值对
        // 键：goto_short_link_key + fullShortUrl -> "goto:short_link:localhost:HSJ6id"
        // 值：requestParam.getOriginUrl() -> "https://nageoffer.com/"
        stringRedisTemplate.opsForValue().set(
                String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                requestParam.getOriginUrl(),
                LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()), TimeUnit.MILLISECONDS
        );
        // 设置布隆过滤器（短链接创建）
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        return ShortLinkCreateRespDTO.builder()
                .gid(requestParam.getGid())
                .originUrl(requestParam.getOriginUrl())
                .fullShortUrl("http://" + shortLinkDO.getFullShortUrl())
                .build();
    }

    @Override
    public ShortLinkCreateRespDTO createShortLinkByLock(ShortLinkCreateReqDTO requestParam) {
        verificationWhiteList(requestParam.getOriginUrl());
        String fullShortUrl;
        // 设置分布式锁方式创建短链接
        RLock lock = redissonClient.getLock(SHORT_LINK_CREATE_LOCK_KEY);
        lock.lock();
        try {
            String shortLinkSuffix = generateSuffixByLock(requestParam);
            fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain)
                    .append(":")
                    .append(port)
                    .append("/")
                    .append(shortLinkSuffix)
                    .toString();
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .domain(createShortLinkDefaultDomain)
                    .originUrl(requestParam.getOriginUrl())
                    .gid(requestParam.getGid())
                    .createdType(requestParam.getCreatedType())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .describe(requestParam.getDescribe())
                    .shortUri(shortLinkSuffix)
                    .enableStatus(0)
                    .totalPv(0)
                    .totalUv(0)
                    .totalUip(0)
                    .delTime(0L)
                    .fullShortUrl(fullShortUrl)
                    .favicon(getFavicon(requestParam.getOriginUrl()))
                    .build();
            ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .gid(requestParam.getGid())
                    .build();
            try {
                baseMapper.insert(shortLinkDO);
                shortLinkGotoMapper.insert(linkGotoDO);
            } catch (DuplicateKeyException ex) {
                throw new ServiceException(String.format("短链接：%s 生成重复", fullShortUrl));
            }

            // 缓存预热
            // 设置 redis（短链接跳转） 缓存键值对
            // 键：goto_short_link_key + fullShortUrl -> "goto:short_link:localhost:HSJ6id"
            // 值：requestParam.getOriginUrl() -> "https://nageoffer.com/"
            stringRedisTemplate.opsForValue().set(
                    String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                    requestParam.getOriginUrl(),
                    LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()), TimeUnit.MILLISECONDS
            );
        } finally {
            lock.unlock();
        }
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://" + fullShortUrl)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .build();
    }


    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        verificationWhiteList(requestParam.getOriginUrl());
        // 根据 requestParam.getFullShortUrl() 查询短链接是否存在
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
        if (hasShortLinkDO == null) {
            throw new ClientException("短链接记录不存在");
        }
        // 判断数据库中 gid 与 requestParam.getGid() 是否一致
        // 如果相等，根据 gid 与 fullShortUrl 更新短链接信息
        if (Objects.equals(hasShortLinkDO.getGid(), requestParam.getGid())) {
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .set(Objects.equals(requestParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()), ShortLinkDO::getValidDate, null);
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .domain(hasShortLinkDO.getDomain())
                    .shortUri(hasShortLinkDO.getShortUri())
                    .favicon(hasShortLinkDO.getFavicon())
                    .createdType(hasShortLinkDO.getCreatedType())
                    .gid(requestParam.getGid())
                    .originUrl(requestParam.getOriginUrl())
                    .describe(requestParam.getDescribe())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .build();
            baseMapper.update(shortLinkDO, updateWrapper);
        } else {
            // 数据库中 gid 与 requestParam.getGid() 不一致
            // 获取读写锁（LOCK_GID_UPDATE_KEY）
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, requestParam.getFullShortUrl()));
            // 获取写锁(排他锁)
            RLock rLock = readWriteLock.writeLock();
            //  加锁
            rLock.lock();
            try {
                //设置数据库中 gid 已失效，并设置死亡时间戳（为已失效记录增加死亡时间戳）
                LambdaUpdateWrapper<ShortLinkDO> linkUpdateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                        .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .eq(ShortLinkDO::getDelTime, 0L)
                        .eq(ShortLinkDO::getEnableStatus, 0);
                ShortLinkDO delShortLinkDO = ShortLinkDO.builder()
                        .delTime(System.currentTimeMillis())
                        .build();
                delShortLinkDO.setDelFlag(1);
                baseMapper.update(delShortLinkDO, linkUpdateWrapper);

                //设置新链接信息
                ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                        .domain(createShortLinkDefaultDomain)
                        .originUrl(requestParam.getOriginUrl())
                        .gid(requestParam.getGid())
                        .createdType(hasShortLinkDO.getCreatedType())
                        .validDateType(requestParam.getValidDateType())
                        .validDate(requestParam.getValidDate())
                        .describe(requestParam.getDescribe())
                        .shortUri(hasShortLinkDO.getShortUri())
                        .enableStatus(hasShortLinkDO.getEnableStatus())
                        .totalPv(hasShortLinkDO.getTotalPv())
                        .totalUv(hasShortLinkDO.getTotalUv())
                        .totalUip(hasShortLinkDO.getTotalUip())
                        .fullShortUrl(hasShortLinkDO.getFullShortUrl())
                        .favicon(getFavicon(requestParam.getOriginUrl()))
                        .delTime(0L)
                        .build();
                baseMapper.insert(shortLinkDO);

                //删除路由表的原始gid记录，并插入新的gid记录
                LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkGotoDO::getGid, hasShortLinkDO.getGid());
                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
                shortLinkGotoMapper.delete(linkGotoQueryWrapper);
                shortLinkGotoDO.setGid(requestParam.getGid());
                shortLinkGotoMapper.insert(shortLinkGotoDO);
            } finally {
                rLock.unlock();
            }
        }
        // 如果数据库中原记录有效期类型不等于请求记录有效期类型
        // 或者数据库中原记录有效期限不等于请求记录有效期限
        // 或者数据库中原记录原始链接不等于请求记录原始链接
        // 则删除 redis（链接跳转）
        if (!Objects.equals(hasShortLinkDO.getValidDateType(), requestParam.getValidDateType())
                || !Objects.equals(hasShortLinkDO.getValidDate(), requestParam.getValidDate())
                || !Objects.equals(hasShortLinkDO.getOriginUrl(), requestParam.getOriginUrl())) {
            stringRedisTemplate.delete(String.format(GOTO_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
            Date currentDate = new Date();
            // 如果数据库中原记录有效期限不为null，且该有效期限小于当前时间（已过期）
            if (hasShortLinkDO.getValidDate() != null && hasShortLinkDO.getValidDate().before(currentDate)) {
                // 如果请求记录有效期类型为永久，或者请求记录有效期限大于当前时间（未过期）
                // 则删除 redis（链接不存在）
                if (Objects.equals(requestParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType())
                        || requestParam.getValidDate().after(currentDate)) {
                    stringRedisTemplate.delete(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
                }
            }
        }
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
        // 分页查询短链接
        IPage<ShortLinkDO> resultPage = baseMapper.pageLink(requestParam);
        return resultPage.convert(each -> {
            ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            result.setDomain("http://" + result.getDomain());
            return result;
        });
    }

    private String generateSuffix(ShortLinkCreateReqDTO requestParam) {
        int customGenerateCount = 0;
        String shortUri;

        while (true) {
            // 最大10次尝试生成短链接后缀
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接生成频繁，请稍后再试。");
            }
            String originUrl = requestParam.getOriginUrl();

            // UUID.randomUUID() 生成 8-4-4-4-12 的十六进制字符分组
            // 例: originUrl = "https://nageoffer.com/"
            // -> "https://nageoffer.com/a9d270a1-30d9-417a-a0e4-7c747aa561ef"
            originUrl += UUID.randomUUID().toString();
            shortUri = HashUtil.hashToBase62(originUrl);
            // 布隆过滤器（短链接创建）中不存在该短链接，则跳出循环
            if (!shortUriCreateCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain + "/" + shortUri)) {
                break;
            }
            customGenerateCount++;
        }
        return shortUri;
    }

    private String generateSuffixByLock(ShortLinkCreateReqDTO requestParam) {
        int customGenerateCount = 0;
        String shorUri;
        while (true) {
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接频繁生成，请稍后再试");
            }
            String originUrl = requestParam.getOriginUrl();
            originUrl += UUID.randomUUID().toString();
            shorUri = HashUtil.hashToBase62(originUrl);
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, createShortLinkDefaultDomain + "/" + shorUri)
                    .eq(ShortLinkDO::getDelFlag, 0);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
            if (shortLinkDO == null) {
                break;
            }
            customGenerateCount++;
        }
        return shorUri;
    }

    @Override
    public List<ShortLinkGroupCountRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                .select("gid as gid,count(*) as shortLinkCount")
                .in("gid",requestParam)
                .eq("enable_status", 0)
                .eq("del_flag", 0)
                .eq("del_time", 0L)
                .groupBy("gid");
        List<Map<String, Object>> shortLinkDOList = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(shortLinkDOList, ShortLinkGroupCountRespDTO.class);
    }

    @SneakyThrows
    private String getFavicon(String url) {
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_OK) {
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }

    @SneakyThrows
    @Override
    public void restoreUrl(String shortUri, ServletRequest servletRequest, ServletResponse servletResponse) {
        // 获得请求头
        String serverName = servletRequest.getServerName();
        // 返回值包装成一个 Optional 对象（端口号）。
        // 作用：避免直接操作可能为 null 的值，提供安全的链式操作。
        String serverPort = Optional.of(servletRequest.getServerPort())
                // 排除端口为 80 的情况
                .filter(each -> !Objects.equals(each, 80))
                // 将端口号转为字符串类型
                .map(String::valueOf)
                // 如果端口号不为空，则拼接冒号和端口号
                // 例： “8003” -> ":8003"
                .map(each -> ":" + each)
                // 否则返回空字符串
                .orElse("");
        // 例："localhost:8003/ecTQF" = "localhost" + ":8003" + "/" + "ecTQF"
//        String fullShortUrl = serverName + serverPort + "/" + shortUri;
        String fullShortUrl = serverName + ":" +  port + "/" + shortUri;

        // 防止缓存击穿【数据缓存中无（数据库中有）或者缓存过期】
        // 寻找 redis（短链接跳转） 的 key
        // 如果 key 存在，则直接跳转目标页面
        String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(originalLink)) {
            shortLinkStats(buildLinkStatsRecordAndSetUser(fullShortUrl, servletRequest, servletResponse));
            ((HttpServletResponse) servletResponse).sendRedirect(originalLink);
            return ;
        }
        // 防止缓存穿透（大量访问数据库不存在数据）
        // 先在布隆（哈希函数，可以快速查询redis）查，后再redis缓存查
        // 下面的布隆过滤器（短链接创建）在短链接创建时创建
        boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);
        if (!contains) {
            ((HttpServletResponse) servletResponse).sendRedirect("/page/notfound");
            return ;
        }
        // 防止缓存穿透（大量访问数据库不存在数据)
        // 查询 redis(短链接空跳) 是否存在
        // 如果存在，则跳转空页面
        String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
            ((HttpServletResponse) servletResponse).sendRedirect("/page/notfound");
            return ;
        }

        // 得到分布式锁（短链接跳转）
        RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        lock.lock();
        try {
            // 防止缓存击穿【数据缓存中无（数据库中有）或者缓存过期】
            // 双重判定锁
            // 查询 redis（短链接跳转） 是否存在
            // 如果存在，则直接跳转目标页面
            originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(originalLink)) {
                shortLinkStats(buildLinkStatsRecordAndSetUser(fullShortUrl, servletRequest, servletResponse));
                ((HttpServletResponse) servletResponse).sendRedirect(originalLink);
                return ;
            }
            // 查询 redis（短链接空跳） 是否存在
            // 如果存在，则跳转空页面
            gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
                ((HttpServletResponse) servletResponse).sendRedirect("/page/notfound");
                return;
            }
            // 根据 fullShortUrl 在路由表查询记录
            // 因为入参只有 fullShortUrl，所以需要在路由表找到分组标识
            LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
            // 防止缓存穿透(大量访问数据库不存在数据)
            // 路由表不存在记录，则设置 redis（短链接空跳），并跳转空页面
            if (shortLinkGotoDO == null) {
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30 , TimeUnit.MINUTES);
                ((HttpServletResponse) servletResponse).sendRedirect("/page/notfound");
                return ;
            }
            // 根据路由表分组标识 gid 查询短链接表
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid,shortLinkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
            // 判断是否缓存穿透【短链接不存在或（短链接存在有效期且已经过期）】
            // 如果属于上述情况，则设置 redis（短链接空跳），并跳转空页面
            if (shortLinkDO == null || (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date()))) {
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30 , TimeUnit.MINUTES);
                ((HttpServletResponse) servletResponse).sendRedirect("/page/notfound");
                return ;
            }
            // 防止缓存击穿
            // 双重判定锁，重构redis（短链接跳转），后续访问短链接时，先查询下面新建的redis（短链接跳转）
            // 设置 redis（短链接跳转）,并跳转目标页面
            stringRedisTemplate.opsForValue().set(
                    String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                    shortLinkDO.getOriginUrl(),
                    LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()),
                    TimeUnit.SECONDS
            );
            shortLinkStats(buildLinkStatsRecordAndSetUser(fullShortUrl, servletRequest, servletResponse));
            ((HttpServletResponse) servletResponse).sendRedirect(shortLinkDO.getOriginUrl());
        } finally {
            lock.unlock();
        }
    }

    @Override
    public ShortLinkBatchCreateRespDTO batchCreateShortLink(ShortLinkBatchCreateReqDTO requestParam) {
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes = requestParam.getDescribes();
        List<ShortLinkBaseInfoRespDTO> result = new ArrayList<>();
        for (int i = 0; i < originUrls.size(); i++) {
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParam, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateReqDTO.setDescribe(describes.get(i));
            try {
                ShortLinkCreateRespDTO shortLink = createShortLink(shortLinkCreateReqDTO);
                ShortLinkBaseInfoRespDTO shortLinkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .fullShortUrl(shortLink.getFullShortUrl())
                        .originUrl(shortLink.getOriginUrl())
                        .describe(describes.get(i))
                        .build();
                result.add(shortLinkBaseInfoRespDTO);
            } catch (Exception e) {
                log.error("批量创建短链接失败, 原始参数： {}", originUrls.get(i));
            }
        }
        return ShortLinkBatchCreateRespDTO.builder()
                .total(result.size())
                .baseLinkInfos(result)
                .build();
    }

    private ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetUser(String fullShortUrl, ServletRequest request, ServletResponse response) {
        // uvFirstFlag:标记当前访问是否是该 uv 的首次访问
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        // 获取当前请求 request 的 Cookie
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        // uv：原子引用，存储 uv 的唯一标识
        AtomicReference<String> uv = new AtomicReference<>();
        Runnable addResponseCookieTask = () -> {
            // 生成新 uv 标识
            uv.set(UUID.fastUUID().toString());
            // 生成名为 uv 的 Cookie
            Cookie uvCookie = new Cookie("uv", uv.get());
            // 设置 Cookie 的有效期（30天）
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            // 设置 Cookie 的路径
            // 例："localhost:8003/ecTQF" -> "ecTQF"
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            // 将生成 Cookie 设置到 response 中
            ((HttpServletResponse) response).addCookie(uvCookie);
            // 首次访问标记设置为 true
            uvFirstFlag.set(Boolean.TRUE);
            // 设置 redis（短链接统计uv）
            stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + fullShortUrl, uv.get());
        };
        // 如果当前 cookies 不为空，则遍历 cookies，查找是否存在 uv Cookie
        if (ArrayUtil.isNotEmpty(cookies)) {
            // 将 Cookie 数组转换为流
            Arrays.stream(cookies)
                    // 过滤出名为 "uv" 的 Cookie
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
                    // 提取 Cookie 的值
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {
                        // 如果 Cookie（uv） 存在，提取 uv 标识并记录到 Redis（短链接统计uv）。
                        uv.set(each);
                        Long uvAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + fullShortUrl, each);
                        uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                    }, addResponseCookieTask);
        } else {
            // Cookie 不存在，则创建 uv Cookie， 并记录 Redis（短链接统计uv）
            addResponseCookieTask.run();
        }
        String remoteAddr = LinkUtil.getActualIp(((HttpServletRequest) request));
        String os = LinkUtil.getOs(((HttpServletRequest) request));
        String browser = LinkUtil.getBrowser(((HttpServletRequest) request));
        String device = LinkUtil.getDevice(((HttpServletRequest) request));
        String network = LinkUtil.getNetwork(((HttpServletRequest) request));
        // uip 首次访问标记设置为 true
        Long uipAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UIP_KEY + fullShortUrl, remoteAddr);
        boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .uv(uv.get())
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(uipFirstFlag)
                .remoteAddr(remoteAddr)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .currentDate(new Date())
                .build();
    }

    @Override
    public void shortLinkStats(ShortLinkStatsRecordDTO statsRecord) {
        Map<String, String> producerMap = new HashMap<>();
        producerMap.put("statsRecord", JSON.toJSONString(statsRecord));
        // 得到统计信息，送入消息队列（短链接统计生产者）
        shortLinkStatsSaveProducer.send(producerMap);
    }
}
